package com.mrzhou.web.support;

import com.mrzhou.core.tool.utils.JacksonUtil;
import com.mrzhou.web.props.HttpClientPoolProperties;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.*;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.protocol.HTTP;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Slf4j
public class HttpClientConfig {

    private final  HttpClientPoolProperties httpClientPoolProperties ;

    private IdleConnectionMonitorThread idleThread = null;

    private HttpClientConfig(HttpClientPoolProperties poolProperties) {
        this.httpClientPoolProperties = poolProperties;
    }

    public static CloseableHttpClient createHttpClient(HttpClientPoolProperties props) {
        try {
            return new HttpClientConfig(props).defaultHttpClient();
        } catch (Exception e) {
            throw new IllegalArgumentException(e);
        }
    }

    public CloseableHttpClient defaultHttpClient() throws Exception {
        SSLContext sslcontext;
        if(httpClientPoolProperties.isIgnoreSSL()) {
            sslcontext = new SSLContextBuilder().loadTrustMaterial(null, (x509Certificates, s) -> true).build();  // 信任所有服务器的证书
        } else {
            sslcontext = SSLContexts.createDefault();
        }
        SSLConnectionSocketFactory factory = new SSLConnectionSocketFactory(sslcontext, new String[]{"TLSv1.2"},
                null, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory())
                .register("https", factory)  // 用来配置支持的协议
                .build();
        // 使用Httpclient连接池的方式配置(推荐)，同时支持netty，okHttp以及其他http框架
        PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
        connManager.setMaxTotal(httpClientPoolProperties.getMaxTotalConnect()); // 最大的连接数量
        connManager.setDefaultMaxPerRoute(httpClientPoolProperties.getMaxPreRoute()); // 默认目标主机的最大连接数

        // 配置基本的请求
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(httpClientPoolProperties.getConnectTimeout())												// 设置连接超时
                .setSocketTimeout(httpClientPoolProperties.getReadTimeout())												   // 设置读取超时
                .setConnectionRequestTimeout(httpClientPoolProperties.getConnectionRequestTimeout())    // 设置从连接池获取连接实例的超时
                .build();

        // 设置请求的连接重试策略
        HttpClientPoolProperties finalHttpClientPoolProperties = httpClientPoolProperties;
        HttpRequestRetryHandler httpRequestRetryHandler = (e, i, httpContext) -> {
            // 重试次数
            if (i >= finalHttpClientPoolProperties.getRetryTimes()) {
                return false;
            }

            // 如果服务器丢掉连接, 那么就重试
            if (e instanceof NoHttpResponseException) {
                return true;
            }

            // 不重试SSL握手异常
            if (e instanceof SSLHandshakeException) {
                return false;
            }

            // 超时异常不重试
            if (e instanceof InterruptedIOException) {
                return false;
            }

            // 服务器不可达, 不重试
            if (e instanceof UnknownHostException) {
                return false;
            }

            // 连接遭到拒绝, 不重试
            if (e instanceof ConnectTimeoutException) {
                return false;
            }

            // SSL握手失败, 不重试
            if (e instanceof SSLException) {
                return false;
            }

            HttpClientContext clientContext = HttpClientContext.adapt(httpContext);
            HttpRequest request = clientContext.getRequest();

            // 如果请求是幂等的，就再次尝试
            return !(request instanceof HttpEntityEnclosingRequest);
        };

        CloseableHttpClient httpClient = HttpClients.custom()
                .setConnectionManager(connManager)
                .setConnectionManagerShared(true)
                .setRetryHandler(httpRequestRetryHandler)
                .setDefaultHeaders(defaultHeaders()) // 设置
                .setDefaultRequestConfig(requestConfig) // 设置基本的请求超时配置
                .setKeepAliveStrategy(connectionKeepAliveStrategy()) // 长连接的保持策略
                .build();

        // 开启守护线程
        // 创建清理线程, 定期主动清理过期/空闲状态中的连接
        if (httpClientPoolProperties.getStartDeamonThread()) {
            idleThread = new IdleConnectionMonitorThread(connManager);
            Thread thread = new Thread(idleThread);
            thread.start();
            log.info("HttpClient的守护线程已开启, 将会监控连接的状态, 定时清理空闲连接");
        }

        return httpClient;

    }

    public List<Header> defaultHeaders() {
        List<Header> headers = new ArrayList<>();
        headers.add(new BasicHeader("User-Agent",
                "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.16 Safari/537.36"));
        headers.add(new BasicHeader("Accept-Encoding", "gzip,deflate"));
        headers.add(new BasicHeader("Accept-Language", "zh-CN"));
        headers.add(new BasicHeader("Connection", "Keep-Alive"));
        return headers;
    }

    private ConnectionKeepAliveStrategy connectionKeepAliveStrategy() {
        return (response, context) -> {
            HeaderElementIterator it = new BasicHeaderElementIterator(response.headerIterator(HTTP.CONN_KEEP_ALIVE));
            while (it.hasNext()) {
                HeaderElement he = it.nextElement();
                log.info("HeaderElement:{}", JacksonUtil.toJsonString(he));
                String param = he.getName();
                String value = he.getValue();
                if (value != null && "timeout".equalsIgnoreCase(param)) {
                    try {
                        return Long.parseLong(value) * 1000;
                    } catch (NumberFormatException ignore) {
                        log.error("解析长连接过期时间异常", ignore);
                    }
                }
            }
            HttpHost target = (HttpHost) context.getAttribute(HttpClientContext.HTTP_TARGET_HOST);
            // 如果请求目标地址, 单独配置长连接保持时间, 使用该配置
            Optional<Map.Entry<String, Integer>> any = Optional.ofNullable(httpClientPoolProperties.getKeepAliveTargetHost()).orElseGet(HashMap::new)
                    .entrySet().stream().filter(e -> e.getKey().equalsIgnoreCase(target.getHostName())).findAny();
            //否则使用默认长连接保持时间
            return any.map(en -> en.getValue() * 1000L).orElse(httpClientPoolProperties.getKeepAliveTime() * 1000L);
        };
    }

    /**
     * 定义一个HttpClient的守护线程,
     * 监控HttpClient线程池的连接的状态;
     * 定期处理过期或空闲状态中的连接, 释放资源
     */
    private class IdleConnectionMonitorThread implements Runnable {
        private final HttpClientConnectionManager connectionManager;

        private volatile boolean exitFlag = false; // volatile 表示该变量是原子性操作

        public IdleConnectionMonitorThread(HttpClientConnectionManager connectionManager) {
            this.connectionManager = connectionManager;
        }

        @Override
        public void run() {
            while (!exitFlag) {
                synchronized (this) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 关闭失效的连接
                connectionManager.closeExpiredConnections();
                // 可选, 如果30秒内不活动则关闭此连接
                connectionManager.closeIdleConnections(httpClientPoolProperties.getInactionThreadTime(), TimeUnit.SECONDS);
            }
        }

        /**
         * 关闭守护线程
         */
        public void shutdown() {
            this.exitFlag = true;
            synchronized (this) {
                notify();
            }
        }

    }

}
